<?php

namespace Pinenut;

class Request {
	const GET = 'GET';
	const POST = 'POST';
	public $action;

	/**
	 * Основные параметры
	 * @var array
	 */
	protected $_params = [];
	
	/**
	 * Изначальный список параметров, до внедрения неявных
	 * @var array
	 */
	protected $_pushParams = [];
	
	/**
	 * Копируются из основного запроса (в контексте) во все остальные, где не проигнорированы
	 * @var unknown
	 */
	protected $_globalParams = [];
	
//	public $deferred;
	public $view; // пусто, для рядомлежащего шаблона...
	              // если используется какой-то другой шаблон - указываем его явно
//	public $method;
//	public $withLayout = false; // для внутренних запросов оболочка не используется

	
	/**
	 * Ключ запроса
	 * Зависит от действия, этапов и явных параметров
	 * @var string
	 */
	protected $_key = null;
	/**
	 * Ключ кеширования
	 * Завиит от действия, этапов, явных и не явных(пулл) параметров
	 * @var string
	 */
	protected $_cacheKey = null;
	
	protected $_preparePushParams = false;
	protected $_preparePullParams = false;
	
	protected $_hasPullParams = null;
	
	protected $_actionClassName = null;
	
	
	/**
	 * Признак корневого запроса
	 * @var boolean
	 */
	protected $_root = false;
	
	/**
	 * Признак генерации лейаута
	 * @var boolean
	 */
	protected $_renderLayout = false;
	
	/**
	 * Признак отложенного запроса
	 * @var boolean
	 */
	protected $_deferred = false;
	

	function __construct($action, array $params = array(), $view = null, $deferred = true) {
		$this->action = $action;
		$this->_params = $params;
//		$this->deferred = $deferred;
		$this->view = $view;
	}
	
	/**
	 * Признак корневого запроса
	 * @return boolean
	 */
	function isRoot($value = null) {
		if (null !== $value) {
			$this->_root = $value;
		}
		return $this->_root;
	}
	
	
	/**
	 * Признак генерации лейаута
	 * @return boolean
	 */
	function isRenderLayout($value = null) {
		if (null !== $value) {
			$this->_renderLayout = $value;
		}
		return $this->_renderLayout;
	}
	
	/**
	 * Признак отложенного запроса
	 * @return boolean
	 */
	function isDeferred($value = null) {
		if (null !== $value) {
			$this->_deferred = $value;
		}
		return $this->_deferred;
	}
	
	/**
	 * Глобальные параметры устанавливаем, получаем
	 * @return array
	 */
	function globalParams($params = null) {
		if (null !== $params) {
			$this->_globalParams = $params;
		}
		return $this->_globalParams;
	}
	
	/**
	 * Возвращае ключ запроса
	 * 
	 * @return string
	 */
	function getKey() {
		if (null === $this->_key) {
			$this->preparePushParams();
			// считаем ключ запроса
			$this->_key = md5 ( $this->action . serialize ( $this->_params ) . serialize ( $this->view ) );
				
		}
		return $this->_key;
	}
	

	/**
	 * Возвращае ключ кеширования учитывающий все зависимости
	 *
	 * @return string
	 */
	function getCacheKey() {
		if (null === $this->_cacheKey) {
	
			$this->getKey();
			
			$this->preparePullParams();
					
			// считаем ключ запроса
			$this->_cacheKey = md5 ( $this->action . serialize ( $this->_params ) . serialize ( $this->view ) );
	
		}
		return $this->_cacheKey;
	}
	
	/**
	 * Определяем есть ли не явные параметры
	 * @return boolean
	 */
	function hasPullParams() {
		if (null === $this->_hasPullParams) {
			$className = $this->getActionClassName ();
			$pullParams = $className::pullParams ();
			$this->_hasPullParams = ! empty ( $pullParams );
		}
		return $this->_hasPullParams;
	}
	
	/**
	 * Подготавливаем массив явных параметров
	 */
	protected function preparePushParams() {
		if ($this->_preparePushParams) {
			return;
		}
				
		$className = $this->getActionClassName ();
		$pushParams = $className::pushParams ();		
		if (! empty ( $pushParams )) {
			// вычисляем не указанные элементы и грохаем их
			$pushParams = array_fill_keys ( ( array ) $pushParams, true );
			$diff = array_diff_key ( $this->_params, $pushParams );
			foreach ( $diff as $key ) {
				unset ( $this->_params [$key] );
			}
		}
		// сохраняем пришедшие и почищеные параметры
		$this->_pushParams = $this->_params;
		
		$this->_preparePushParams = true;
	}
	
	/**
	 * Добавляем к явным параметрам неявные
	 */
	protected function preparePullParams() {
		if ($this->_preparePullParams) {
			return;
		}
		
		$className = $this->getActionClassName ();
		$pullParams = $className::pullParams ();
		if (! empty ( $pullParams )) {
			// если есть неявные параметры - вызываем соответствующие методы запроса
			foreach ((array)$pullParams as $pullName=>$pullParams) {
				if (is_int($pullName)) {
					$pullName = $pullParams;
				}
				
				$methodName = 'pull'.ucfirst($pullName);
				if (method_exists($this, $methodName)) {
					$this->_params = array_merge($this->_params, (array)$this->$methodName($pullParams));
				}
			}				
		}		
		
		$this->_preparePullParams = true;
	}
	
	
	/**
	 * Магический геттер параметров
	 * 
	 * @param string $name        	
	 * @return multitype:
	 */
	function __get($name) {
		if (array_key_exists ( $name, $this->_params )) {
			return $this->_params [$name];
		}
	}
	
	/**
	 * Магический сеттер параметра
	 * 
	 * @param string $name        	
	 * @param multitype $value        	
	 * @return multitype:
	 */
	function __set($name, $value) {
		return $this->_params [$name] = $value;
	}
	/**
	 * Устанавливаем и получаем сразу все параметры запроса
	 * 
	 * @param array $params        	
	 * @return multitype:
	 */
	function params(array $params = null) {
		if (null !== $params) {
			$this->_params = $params;
		}
		return $this->_params;
	}	
	
	
	/**
	 * Формируем имя класса действия, с проверками
	 * Преобразование названия действия в имя класса
	 * По умолчанию используются namespace, и в нем класс, с заглавной буквы и постфиксом "Action.php"
	 *
	 * @return \Pinenut\Action
	 */
	public function getActionClassName() {
		
		if (null === $this->_actionClassName) {
			$this->_actionClassName = static::actionNameToClassName($this->action);
		}
		return $this->_actionClassName;
	}
	
	/**
	 * Техническое преобразование имени действия в имя класса
	 * @param string $action
	 * @throws \Exception
	 * @return string
	 */
	public static function actionNameToClassName($action) {
		// проверяем, что это запросы и что классы существуют и они - запросы
		$path = array_map('static::prepareClassNamePart', explode('.', $action));
		$className = implode('\\', $path).'Action';
			
		if (!class_exists($className)) {
			throw new \Exception("Action '".htmlspecialchars($action).
				"' ($className) not found!", 404);

		}
		
		return $className;
		
	}
	
	
	/**
	 * Если нужно поменять тип преобразования - это можно сделать тут
	 * @param unknown $part
	 * @return string
	 */
	static protected function prepareClassNamePart($part) {
		return ucfirst($part);
	}
	
	/**
	 * Создаем экземпляр действия, за одно проверяем корректность
	 * @throws \Exception
	 * @return \Pinenut\Action
	 */
	public function getAction() {
		
		// надо неявные параметры внедрить, если забыли...
		$this->preparePullParams();
		
		$className = $this->getActionClassName();
		// TODO: проверить реакцию, если класс с неподходящим конструктором
		$action = new $className($this);
		if ($action instanceof \Pinenut\Action) {
			return $action;
		} else {
			throw new \Exception("Action '".htmlspecialchars($request->action).
					"' ($className) not support!", 500);
		}
	}

	public function canExecute()
	{
		/** @var \Action $className */
		$className = $this->getActionClassName();
		return !$this->isRoot() || $className::getIsPublic();
	}
	
	
	/**
	 * Возвращаем этапы выполнения действия
	 * 
	 * @return multitype:
	 */
	public function getStages() {
		$className = $this->getActionClassName();
		return $className::stages($this);
	}
	

	//////////////////////////////////////////////////////////////////////////
	
	/**
	 * Идентификатор текущего пользователя
	 * @return array
	 */
	function pullIdentity() {
		return ['_identity' => \Application::auth()->identity()];
	}
	
	function pullIp()
	{
		return ['_ip' => $_SERVER['REMOTE_ADDR']];
	}

	/**
	 * Параметр контрольной суммы браузерного кеша
	 * @return multitype:Ambigous <NULL, unknown> 
	 */
	function pullHttpIfNoneMatch() {
		return ['_httpIfNoneMatch' => isset($_SERVER["HTTP_IF_NONE_MATCH"])
			?$_SERVER["HTTP_IF_NONE_MATCH"]:null];
	}
	
	/**
	 * Параметр времени браузерного кеша
	 * @return multitype:Ambigous <NULL, unknown> 
	 */
	function pullHttpIfModifiedSince() {
		return ['_httpIfModifiedSince' => isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])
			?$_SERVER["HTTP_IF_MODIFIED_SINCE"]:null];
	}	
	
	/**
	 * Признак того, что это пост-запрос
	 */
	function pullIsPost() {
		return ['_isPost' => $_SERVER ['REQUEST_METHOD'] == static::POST];
	}
	
	/**
	 * Признак того, что это аякс
	 */
	function pullIsAjax() {
		return ['_isAjax' =>  isset ( $_SERVER ['HTTP_X_REQUESTED_WITH'] ) && $_SERVER ['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'];
	}
	
	/**
	 * возвращаем идентификаор сессии
	 */
	function pullSessionId() {
		return ['_sessionId' =>  \Session::getId()];
	}
	
	
	/**
	 * Достаём основной запрос
	 */
	function pullMainRequest() {
		$request = \Dispatcher::$currentContext->mainRequest;
		return ['_mainRequestAction' => $request->action, '_mainRequestParams' => $request->_pushParams];
	}
	
	/**
	 * Генерим случайное число из диапазона
	 */
	function pullRand(array $params = []) {
		if (isset($params[0]) && isset($params[0])) {
			return ['_rand' => rand($params[0], $params[1])];
		} else {
			return ['_rand' => rand()];
		}
	}
	
	
	// вся эта лабудень должна быть преобразована в получение пулл-параметров	
	static function isPost() {
		// вот не знаю пока как метод подтягивать
		return $_SERVER ['REQUEST_METHOD'] == static::POST;
	}
	static function isAjax() {
		// from yii
		return isset ( $_SERVER ['HTTP_X_REQUESTED_WITH'] ) && $_SERVER ['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
	}
	
	/**
	 * Возвращаем данные сессии
	 * Как реализовывать - сложный вопрос
	 * Помимо этого надо не забыть про время блокировки в процессе работы с сессией,
	 * и как минимум стартовать её принудительно и на непродолжительный срок
	 *
	 * @param string $key        	
	 * @return unknown
	 */
// 	static function getSession($key = null) {
// 		return \Session::getValue($key);
// 	}
// 	static function getSessionId() {
// 		return \Session::getId();
// 	}
}